/******************************************************************************/
#include "stdafx.h"
#include "Main.h"
/******************************************************************************/
//#define LOG // define this to enable logging for debug purposes
/******************************************************************************/
Client::~Client()
{
   // client is removed so add client connection usage stats to the server
   Server.bytes_sent    +=connection.sent    ();
   Server.bytes_received+=connection.received();

   connection.del(); // delete connection before logging out so no more data is sent

   logout();
}
Client::Client()
{
   connection_version_ok=false;
   time_difference=0;
}
/******************************************************************************/
// OPERATIONS
/******************************************************************************/
void Client::leaveGame()
{
   // save data first before leaving the game
   if(inGame() && name.is())
   {
      File f; f.writeMem(); save(f); f.pos(0); SafeOverwrite(f, StorageCharFile(name)); // save character data to the character storage folder
   }

   NetChr::leaveGame(); // call base method
}
/******************************************************************************/
void Client::logout()
{
   leaveGame();   // leave game
   account.del(); // clear active account info
}
/******************************************************************************/
void Client::set(ChrInfo &ci)
{
   ci.addr =connection.address();
   ci.name =name;
   ci.scale=scale;
   ci.mesh =mesh;
}
void Client::set(ChrVisuals &cv)
{
   cv.addr=connection.address();
   cv.inv =inv;
}
void Client::set(ChrPos &cp)
{
   cp.addr     =connection.address();
   cp.pos      =pos();
   cp.angle    =angle;
   cp.stop_move=stop_move;
   cp.walk_run =walk_run;
}
void Client::operator=(ChrPos &cp)
{
   pos      (cp.pos);
   angle    =cp.angle;
   stop_move=cp.stop_move;
   walk_run =cp.walk_run;
}
/******************************************************************************/
// CALLBACKS
/******************************************************************************/
void Client::create(ConnectionServer &server)
{
   connection.tcpNoDelay(true);
}
/******************************************************************************/
void Client::connected(Obj &obj)
{
   if(Client *client=CAST(Client, &obj))
   {
      // we've got connected to 'client' so we need to send basic, visual and position data to him
      ChrInfo    ci; set(ci); ServerSendChrConnect(client->connection, ci);
      ChrVisuals cv; set(cv); ServerSendChrVisuals(client->connection, cv);
      ChrPos     cp; set(cp); ServerSendChrPos    (client->connection, cp);
   }
}
void Client::disconnected(Obj &obj)
{
   // we've got disconnected from 'client' so let's send him a message that we're no longer neighbors (he can remove us from the neighbor list)
   if(Client *client=CAST(Client, &obj))ServerSendChrDisconnect(client->connection, connection.address());
}
/******************************************************************************/
// NETWORK
/******************************************************************************/
void Client::distributeToNeighbors(File &f, bool skip_direct_connections)
{
   if(f.size())REP(neighbors())
   {
      Net::Neighbor &neighbor=T.neighbor(i);
      if(skip_direct_connections ? !neighbor.direct_connection : true) // clients will exchange data manually (without involving the server)
         if(Client *client=CAST(Client, &neighbor.obj()))
            {f.pos(0); client->connection.dataFull(f, f.left(), false);}
   }
}
/******************************************************************************/
// UPDATE
/******************************************************************************/
Bool Client::update()
{
   if(!ConnectionServer::Client::update())return false;

   REP(8)if(!connection.receive(0))return true;else // process 8 commands at once
   {
      Byte cmd=connection.data.getByte(); // first byte is always the command

      // version must be checked first, before all other commands
      if(cmd==CS_VERSION_CHECK)ServerSendVersion(connection, connection_version_ok=ServerRecvVersion(connection.data));
      if(!connection_version_ok)return false; // if client/server use different versions then don't continue but terminate the connection instead

      // at this stage we're sure that the version is ok, so check other commands
      switch(cmd)
      {
         case CS_INIT_CONN:
         {
            DateTime client_utc;
            ServerRecvInitConnection(connection.data, time_difference, client_utc);
         #ifdef LOG
            DateTime server_utc; server_utc.getUTC();
            LogN(S+"Local: "+DateTime().getLocal().asText(true)+", Init: Address: "+connection.address().asText()+", ClientUTC: "+client_utc.asText(true)+", ServerUTC: "+server_utc.asText(true)+", TimeDifference: "+(client_utc-server_utc));
         #endif
            ServerSendInitConnection(connection); // send reply
         }break;

         case CS_TIME_TEST:
         {
            Flt cur_time; ServerRecvTimeTest(connection.data, cur_time);
            Flt difference=cur_time-Time.curTime(); // get new time difference
            if(Abs(difference-time_difference)>10) // if new time difference exceeds 10 seconds then it's very possible that client uses speedhack
            {
               LogN(S+"Local: "+DateTime().getLocal().asText(true)+", Speedhack: Address: "+connection.address().asText()+", Account: "+account.name+", Character: "+name+", TimeDifference: "+(difference-time_difference)+", ConnectionLife: "+connection.life()/1000);
               return false; // delete the client
            }
         #ifdef LOG
            LogN(S+"Local: "+DateTime().getLocal().asText(true)+", TimeTest: Address: "+connection.address().asText()+", Account: "+account.name+", Character: "+name+", TimeDifference: "+(difference-time_difference)+", ConnectionLife: "+connection.life()/1000);
         #endif
         }break;

         case CS_DEBUG:
         {
            Str message; ServerRecvDebug(connection.data, message);
            LogN(S+"Local: "+DateTime().getLocal().asText(true)+", Debug: Address: "+connection.address().asText()+", Account: "+account.name+", Character: "+name+", ConnectionLife: "+connection.life()/1000+", Message: "+message);
         }break;

         case CS_REGISTER:
         {
            logout();

            Str  name, password, email; ServerRecvRegister(connection.data, name, password, email);
            Bool ok=false;
         #ifdef LOG
            Log(S+"Local: "+DateTime().getLocal().asText(true)+", Register: Address: "+connection.address().asText()+", ServerUTC: "+DateTime().getUTC().asText(true)+", Name: "+name+", Result: ");
         #endif
            if(ValidName    (name    ))
            if(ValidPassword(password))
            {
               Str file=StorageAccountFile(name);
               if(!FExistStd(file)) // if there's no account yet with that name
               {
                  File f; f.writeMem(); Account account(name, password, email); account.save(f); f.pos(0);
                  ok=SafeOverwrite(f, file);
               }
            }
         #ifdef LOG
            LogN(S+ok);
         #endif
            ServerSendRegister(connection, ok); // send reply
         }break;

         case CS_LOGIN:
         {
            logout();

            Str name, password; ServerRecvLogin(connection.data, name, password);
            LOGIN_RESULT result;
         #ifdef LOG
            Log(S+"Local: "+DateTime().getLocal().asText(true)+", Login: Address: "+connection.address().asText()+", ServerUTC: "+DateTime().getUTC().asText(true)+", Name: "+name+", Result: ");
         #endif
            if(!ValidName    (name    ))result=LOGIN_INVALID_USER_NAME;else
            if(!ValidPassword(password))result=LOGIN_INVALID_PASSWORD ;else
            {
               Str     file=StorageAccountFile(name);
               Account account;
               if(!account.load(file                      ))result=LOGIN_USER_DOES_NOT_EXIST       ;else
               if(!Equal       (account.password, password))result=LOGIN_USER_OK_INCORRECT_PASSWORD;else
               {
                  REPA(Server.clients)if(Server.client(i).account.name==name){result=LOGIN_USER_ALREADY_LOGGED; goto already_logged;} // test if any other client is logged with the same account
                  T.account=account;
                  result=LOGIN_USER_OK_PASSWORD_OK;
               already_logged:;
               }
            }
         #ifdef LOG
            LogN(S+result);
         #endif
            ServerSendLogin(connection, result);
         }break;

         case CS_LOGOUT:
         {
         #ifdef LOG
            Str n; FREP(neighbors())if(Client *client=CAST(Client, &neighbor(i).obj()))n+=S+client->name+'('+neighbor(i).direct_connection+") ";
            LogN(S+"Local: "+DateTime().getLocal().asText(true)+", Logout: Address: "+connection.address().asText()+", Account: "+account.name+", Character: "+name+", ConnectionLife: "+connection.life()/1000+", Neighbors: "+neighbors()+" ("+n+")");
         #endif
            logout();
         }break;

         case CS_LIST_CHRS:
         {
            Memc<NetChr> chars; if(loggedIn())REPA(account.chars)if(!chars.New().load(StorageCharFile(account.chars[i])))chars.removeLast();
            ServerSendListChrs(connection, chars);
         }break;

         case CS_CREATE_CHR:
         {
            leaveGame();

            ChrCreateRequest  ccr; ServerRecvCreateChr(connection.data, ccr);
            CHR_CREATE_RESULT result;
            if(!loggedIn()                            )result=CHR_CREATE_NOT_LOGGED_IN;else
            if( account.chars.elms()>=MAX_ACCOUNT_CHRS)result=CHR_CREATE_NO_ROOM      ;else // if account has reached maximum character limit
            if(!ccr.valid()                           )result=CHR_CREATE_INVALID_DATA ;else
            {
               Str file=StorageCharFile(ccr.name);
               if(     FExistStd(file))result=CHR_CREATE_NAME_TAKEN  ;else // character name not yet taken
               if(!NetChr::create(ccr))result=CHR_CREATE_INVALID_DATA;else // create character
               {
                  File f; f.writeMem();

                  // first save character file (to block the file, and disallow any other account to use that file)
                  f.reset(); save(f); f.pos(0); SafeOverwrite(f, file);

                  // then save account file
                  account.chars.add(ccr.name);
                  f.reset(); account.save(f); f.pos(0); SafeOverwrite(f, StorageAccountFile(account.name));

                  result=CHR_CREATE_OK;
               }
            }
            ServerSendCreateChr(connection, result);
         }break;

         case CS_DELETE_CHR:
         {
            leaveGame();

            Str  name, password; ServerRecvDeleteChr(connection.data, name, password);
            Bool ok=false;
            if(loggedIn()             )
            if(ValidName    (name    )) // valid character name
            if(ValidPassword(password))if(account.password==password) // password matches
               REPA(account.chars)if(account.chars[i]==name) // search for character
            {
               account.chars.remove(i, true);
               File f; f.writeMem(); account.save(f); f.pos(0); SafeOverwrite(f, StorageAccountFile(account.name)); // first overwrite the account
               FDel(StorageCharFile(name)); // then delete the character
               ok=true;
               break;
            }
         }break;

         case CS_ENTER_GAME:
         {
            leaveGame();

            Str  name; ServerRecvEnterGame(connection.data, name); // get desired character name
            Bool ok=false;
            if(loggedIn())
               REPA(account.chars)if(Equal(account.chars[i], name)) // search for character
            {
               ok=load(StorageCharFile(name));
               break;
            }
            ServerSendEnterGame(connection, ok, T); // send reply
            if(ok)enterGame(); // enter game after sending reply
         }break;

         case CS_LEAVE_GAME:
         {
         #ifdef LOG
            Str n; FREP(neighbors())if(Client *client=CAST(Client, &neighbor(i).obj()))n+=S+client->name+'('+neighbor(i).direct_connection+") ";
            LogN(S+"Local: "+DateTime().getLocal().asText(true)+", LeaveGame: Address: "+connection.address().asText()+", Account: "+account.name+", Character: "+name+", ConnectionLife: "+connection.life()/1000+", Neighbors: "+neighbors()+" ("+n+")");
         #endif
            leaveGame();
         }break;

         case CS_BYE: return false; // client disconnects (client application shut down occured), return false to delete the client
      }

      // in game commands only
      if(inGame())switch(cmd)
      {
         case CS_CHR_POS: // client updated his position
         {
            ChrPos cp; ServerRecvChrPos(connection, cp);
            T=cp;
            File f; ServerWriteChrPos(f, cp); distributeToNeighbors(f, true);
         }break;

         case CS_CHR_ANIM: // client started an animation
         {
            Str  anim; ServerRecvChrAnim (connection.data, anim);
            File f   ; ServerWriteChrAnim(f, connection.address(), anim); distributeToNeighbors(f, true);
         }break;

         case CS_CHR_INV_SLOTS: // character changed his equipment
         {
            ServerRecvChrInvSlots(connection.data, inv.slots);
            ChrVisuals cv; set(cv);
            File f; ServerWriteChrVisuals(f, cv); distributeToNeighbors(f);
         }break;

         case CS_CHAT: // client said something
         {
            Str  text; ServerRecvChat (connection.data, text); text.clip(MAX_CHAT_LENGTH);
            File f   ; ServerWriteChat(f, connection.address(), text); distributeToNeighbors(f);
         }break;
      }
   }
   return true;
}
/******************************************************************************/
